package org.icop.frame.search.virtualdoc;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.icop.frame.search.Searcher;
import org.icop.model.MultiSet;
import org.icop.model.ValuedCorrespondence;
import org.icop.utils.GraphDistanceCache;
import org.icop.virtdoc.VirtualDoc;
import org.jbpt.algo.graph.DirectedGraphAlgorithms;
import org.jbpt.algo.graph.DistanceRelation;
import org.jbpt.graph.abs.IDirectedEdge;
import org.jbpt.graph.abs.IDirectedGraph;
import org.jbpt.hypergraph.abs.IVertex;

public class StandardVirtualDocForDistanceSetSearcher<G extends IDirectedGraph<E,V>, E extends IDirectedEdge<V>, V extends IVertex> extends AbstractVirtualDocSearcher<G, E, V> implements Searcher<G, E, V> {

	protected int maxGroupDistance;
	
	Map<G, Float> distanceRatios;
	Map<G, Set<V>> sourceNodes;
		
	public StandardVirtualDocForDistanceSetSearcher(int maxGroupDistance, double cutoff, boolean doWrapup, boolean noNtoMMatches, boolean no1toNMatches) {
		super(cutoff,doWrapup, noNtoMMatches, no1toNMatches);
		this.maxGroupDistance = maxGroupDistance;
		this.distanceRatios = new HashMap<G, Float>();
		this.sourceNodes = new HashMap<G, Set<V>>();
	}

	/**
	 * Override the search method in order to retrieve additional information
	 * on the graphs, before the actual search is started.
	 */
	public MultiSet<ValuedCorrespondence<V>> search(G sg1, G sg2, Collection<V> nodesInSg1, Collection<V> nodesInSg2, MultiSet<ValuedCorrespondence<V>> matches) {
		
		@SuppressWarnings("unchecked")
		DistanceRelation<E, V> distanceRelation1 = (DistanceRelation<E, V>) GraphDistanceCache.getInstance().getDistanceRelationForGraph((IDirectedGraph<IDirectedEdge<IVertex>, IVertex>) sg1);
		@SuppressWarnings("unchecked")
		DistanceRelation<E, V> distanceRelation2 = (DistanceRelation<E, V>) GraphDistanceCache.getInstance().getDistanceRelationForGraph((IDirectedGraph<IDirectedEdge<IVertex>, IVertex>) sg2);
		
		float maxDistanceG1 = distanceRelation1.getMaxDistance();
		float maxDistanceG2 = distanceRelation2.getMaxDistance();
		
		this.distanceRatios.put(sg1, maxDistanceG1 / maxDistanceG2);
		this.distanceRatios.put(sg2, maxDistanceG2 / maxDistanceG1);
		
		/*
		 * Source nodes are not cached in the graph, therefore, we cache them here
		 */
		DirectedGraphAlgorithms<E, V> dga = new DirectedGraphAlgorithms<>();
		this.sourceNodes.put(sg1,dga.getSources(sg1));
		this.sourceNodes.put(sg2,dga.getSources(sg2));
		
		return super.search(sg1,sg2,nodesInSg1,nodesInSg2,matches);
	}
	
	@Override
	protected void createDocsForNodeSets(G g, Map<Set<V>, VirtualDoc> nodesToVirtualDoc, Set<VirtualDoc> docs) {
		
		@SuppressWarnings("unchecked")
		DistanceRelation<E, V> distanceRelation = (DistanceRelation<E, V>) GraphDistanceCache.getInstance().getDistanceRelationForGraph((IDirectedGraph<IDirectedEdge<IVertex>, IVertex>) g);
		
		Set<Set<V>> nodeSets = new HashSet<>();		
		
		int currentDistanceForBaseNode = 0;
		boolean reachedMax = false;
		
		while (!reachedMax) {
			if (currentDistanceForBaseNode > distanceRelation.getMaxDistance())
				reachedMax = true;
			
			/*
			 * Select base nodes that are in a current distance from the source nodes
			 */
			Set<V> baseNodes = getNodesInDistanceFromSourceNodes(g, currentDistanceForBaseNode);
			for (V n : baseNodes){
				/*
				 * For a base node: iterate over different group distances
				 */
				for (int i = 1; i <= maxGroupDistance; i++) {
					/*
					 * Case GD-S(n) for sequences
					 */
					for (List<V> path : getNodesOnDirectedPathInDistance(g, n, i)) {
						Set<V> gdSSet = new HashSet<>(path);
						gdSSet.retainAll(super.nodesInGraph.get(g));
						if (!gdSSet.isEmpty())
							nodeSets.add(gdSSet);
					}
										
					/*
					 * Case GD-F(n) for splits
					 */
					Set<V> gdFSetWo = new HashSet<>(distanceRelation.getVerticesInDistanceFromVertex(n, i));
					gdFSetWo.retainAll(super.nodesInGraph.get(g));
					if (!gdFSetWo.isEmpty())
						nodeSets.add(gdFSetWo);
					Set<V> gdFSet = new HashSet<>(gdFSetWo);
					gdFSet.add(n);
					gdFSet.retainAll(super.nodesInGraph.get(g));
					if (!gdFSet.isEmpty())
						nodeSets.add(gdFSet);

					/*
					 * Case GD-B(n) for joins
					 */
					Set<V> gdBSetWo = new HashSet<>(distanceRelation.getVerticesInDistanceToVertex(n, i));
					gdBSetWo.retainAll(super.nodesInGraph.get(g));
					if (!gdBSetWo.isEmpty())
						nodeSets.add(gdBSetWo);
					Set<V> gdBSet = new HashSet<>(gdBSetWo);
					gdBSet.add(n);
					gdBSet.retainAll(super.nodesInGraph.get(g));
					if (!gdBSet.isEmpty())
						nodeSets.add(gdBSet);

					/*
					 * Case GD-(n) for arbitrary groups
					 */
					Set<V> gdSet = new HashSet<>(distanceRelation.getVerticesInDistanceToAndFromVertex(n, i));
					gdSet.add(n);
					gdSet.retainAll(super.nodesInGraph.get(g));
					if (!gdSet.isEmpty())
						nodeSets.add(gdSet);
				}
			}
			currentDistanceForBaseNode++;
		}
		
		/*
		 * Generate docs for sets of nodes 
		 */
		for (Set<V> nodeSet : nodeSets) {
			if (nodesToVirtualDoc.containsKey(nodeSet))
				continue;
			
			VirtualDoc doc = getVirtualDocForNodes(g, nodeSet);
			nodesToVirtualDoc.put(nodeSet, doc);
			docs.add(doc);
		}
	}
	
	protected Set<List<V>> getNodesOnDirectedPathInDistance(G g, V node, int distance) {
		Set<List<V>> result = new HashSet<>();
		/*
		 * Abort recursion
		 */
		if (distance == 0) {
			List<V> path = new ArrayList<>();
			path.add(node);
			result.add(path);
			return result;
		}
		
		@SuppressWarnings("unchecked")
		DistanceRelation<E, V> distanceRelation = (DistanceRelation<E, V>) GraphDistanceCache.getInstance().getDistanceRelationForGraph((IDirectedGraph<IDirectedEdge<IVertex>, IVertex>) g);

		for(V nextNode : distanceRelation.getVerticesInDistanceFromVertex(node, 1)) {
			int newDistance = distance - 1;
			for (List<V> subPath : getNodesOnDirectedPathInDistance(g, nextNode, newDistance)) {
				subPath.add(0, node);
				result.add(subPath);
			}
		}
		
		return result;
	}

	protected Set<V> getNodesInDistanceFromSourceNodes(G g, int distance) {
		Set<V> result = new HashSet<>();
		for (V s : this.sourceNodes.get(g)) {
			@SuppressWarnings("unchecked")
			DistanceRelation<E, V> distanceRelation = (DistanceRelation<E, V>) GraphDistanceCache.getInstance().getDistanceRelationForGraph((IDirectedGraph<IDirectedEdge<IVertex>, IVertex>) g);
			result.addAll(distanceRelation.getVerticesInDistanceFromVertex(s, distance));
			
			/*
			 * Distance = 0, requires to add source nodes, as these
			 * nodes are not considered in the step above
			 */
			if (distance == 0)
				result.add(s);
		}
		return result;
	}

}
